Глава 20

ПОТОКИ

Основным способом хранения данных в Turbo Vision является их размещение в полях объектов. Это, разумеется, не означает, что Ваша программа должна использовать исключительно поля объектов и/или методы доступа к ним - программа в среде Turbo Vision - это прежде всего программа на Турбо Паскале, а следовательно, Вы можете использовать в ней и все другие средства этой системы программирования. Однако в ходе изучения Turbo Vision Вы уже, очевидно, смогли по достоинству оценить мощные возможности, предоставляемые техникой объектно-ориентированного программирования и средой Turbo Vision, и, надеюсь, вряд ли захотите широко использовать в своей программе неинкапсулированные данные.

Для сохранения данных на диске с целью последующего их считывания в программу поля можно тем или иным способом выделить из объектов и затем использовать обычные средства доступа к файлам. Согласитесь, что такой прием нельзя считать естественным для объектно-ориентированной программы. Во всяком случае следует учесть, что потоки - а их изучению посвящена эта глава - разработаны прежде всего для сохранения в файлах инкапсулированных данных, т.е. полей объектов.

20.1. СУЩНОСТЬ ПОТОКОВ

Поток Turbo Vision - это коллекция объектов со специфичным способом хранения элементов: для этих целей коллекция-поток использует дисковый файл, отображаемую память (EMS-память) или файловое устройство (устройство ввода-вывода данных).

Весьма удобной моделью потока может служить обычный дисковый файл с прямым или последовательным доступом. В дальнейшем, говоря о потоках, я буду в основном иметь в виду эту модель, а там, где это необходимо, уточнять детали доступа к EMS-памяти и к файловым устройствам.

Как известно, в Турбо Паскале могут использоваться типизированные или нетипизированные файлы. При работе с типизированными файлами Турбо Паскаль осуществляет необходимый котроль за типом данных, предупреждая Вас о возможных ошибках; типизированные файлы как правило не обеспечивают высокую скорость доступа к данным. При обращении к нетипизированным файлам контроль возлагается целиком на программиста, но в этом случае обычно существенно увеличивается скорость работы с файлами. Потоки Turbo Vision обеспечивают в известном смысле компромисс между обоими способами хранения информации в файле: они позволяют компилятору осуществить проверку типов на уровне проверки объектов, но не контролируют сами данные, помещаемые в файл, т.к. в правильно сконструированной программе объект сам осуществляет необходимые операции с диском и делает это безошибочно и очень быстро.

Основным отличием потоков от файлов является их полиморфизм. Как и коллекции, потоки Turbo Vision могут быть полиморфными, что означает, что в одном потоке могут сохраняться разные объекты (точнее, их поля). Как программист, знакомый с техникой объектно-ориентированного программирования, Вы должны понимать, что записать в один файл несколько разных объектов не составляет особой проблемы - для этого нужно лишь вызвать соответствующий виртуальный метод, который знает, какие поля и в какой последовательности помещаются в файл. Но вот каким образом поддерживается полиморфизм при чтении данных? Как из общего потока выделяются данные, относящиеся к конкретному объекту? Эту проблему еще можно было бы решить, если бы потоки обеспечивали только последовательный доступ к данным - тогда мы могли бы просто вызывать виртуальные методы чтения данных в том же порядке, в каком объекты записывались в файл. Но потоки Turbo Vision предоставляют также и произвольный способ доступа к данным!

Решение проблемы разработчиками Turbo Vision найдено в особом механизме, называемом регистрация объектов. Зарегистрировать объект - это означает приписать объекту некоторый уникальный регистрационный номер. Регистрационный номер записывается в файл (а следовательно и считывается из него) первым. Прочитав регистрационный номер, Turbo Vision однозначно определяет объект, которому принадлежат данные, и вызывает соответствующий виртуальный метод доступа к файлу.

Таким образом, для работы с потоками необходимо:

Важной особенностью доступа к потокам является возможность записи в них групп и соответственно чтения групп из потока. При этом группа сама будет автоматически вызывать нужную процедуру доступа для каждого из своих элементов, что значительно упрощает работу с потоком.

20.2. РЕГИСТРАЦИЯ ОБЪЕКТОВ

Для регистрации любого объекта используется обращение к глобальной процедуре Register-Type, определенной в интерфейсной части модуля Objects. Единственным параметром обращения к этой процедуре является запись типа TStreamRec, в которой группируются некоторые важные для Turbo Vision характеристики объекта:

type

PStreamRec = ATStreamRec; 

TStreamRec = record

ObjType: Word; {Регистрационный номер объекта}

VMTLink: Word; {Смещение таблицы виртуальных методов}

Load : Pointer; {Адрес метода чтения}

Store : Pointer; {Адрес метода записи}

Next : Word; {Связь в списке} 

end;

Для каждого объекта Вашей программы (как стандартного, так и нестандартного) должна быть создана своя запись типа TStreamRec, если только Вы собираетесь помещать соответствующий объект в поток или получать его из потока. Однако для стандартных объектов такие записи уже существуют и Вам нет нужды создавать их заново: по принятому в Turbo Vision соглашению запись TStreamRec для стандартного объекта имеет такое же имя, как имя объекта, с заменой начальной буквы Т на R. Например, для TWindow регистрационная запись называется RWindow, для TDialog -RDialog и т.д. Имеет смысл следовать этому соглашению, определяя идентификаторы регистрационных записей для нестандартных объектов.

Итак, для каждого нестандартного объекта Вы должны подготовить запись TStreamRec, причем фактически определяются только первые четыре поля этой записи, так как поле Next используется для организации связанного списка и заполняется внутри процедуры RegisterType.

В поле ObjType Вы должны поместить константу-идентификатор объекта. В Turbo Vision константы 0...99 уже используются в стандартных регистрационных записях RXXXX, таким образом, если Вы собираетесь использовать стандартные регистрационные записи, в Вашем распоряжении остаются константы в диапазоне от 100 до 65535. Выбор констант из этого диапазона может быть произвольным, однако Turbo Vision требует, чтобы каждый объект характеризовался уникальной константой. Заметим, что Turbo Vision следит за уникальностью регистрационных констант и аварийно завершает исполнение программы (код ошибки 212), если в программе делается попытка зарегистрировать два разных объекта с одинаковыми регистрационными номерами.

Поле VMTLink должно содержать смещение адреса таблицы виртуальных методов (ТВМ) объекта. Турбо Паскаль имеет стандартную функцию TypeOf (Type: object) , которая возвращает адрес ТВМ для указанного типа Туре объекта, поэтому поле VMTLink обычно заполняется значением Ofs (TypeOf (TName)^), где TName - имя нестандартного объекта.

Поля Load и Store должны содержать адреса соответствующих виртуальных методов (см. п.20.3), обеспечивающих чтение объектов из потока и их запись в поток.

Например, если в программе определен объект

type

TMyWindow = object (TWindow)

.....

Constructor Load(var S: TStream); 

Procedure Store(var S: TStream); 

end ; 

то для его регистрации создается запись

const

RMyWindow: TStreamRec = ( 

ObjType: 100;

VMTLink: Ofs(TypeOf(TMyWindow)^); 

Load : @TMyWindow.Load; 

Store : @TMyWindow.Store);

Обычно регистрация осуществляется в конструкторе Init вновь созданного объекта, например:

Constructor TMyWindow.Init; 

begin

RegisterType(RMyWindow);

.....

end;

Однако, если в программе предполагается помещать в поток (или считывать из потока) объекты разного типа, регистрацию объектов как правило реализуют отдельной процедурой, вызываемой в конструкторе Init программы или в любом другом удобном месте, но перед фактическим обращением к потоку. Например:

Procedure RegisterMyProgram;

begin

RegisterType(RWindow); {Регистрация стандартного объекта} 

RegisterType(RMyWindow); {Регистрация нового объекта}

.....

end;

Для упрощения регистрации стандартных объектов в модулях Turbo Vision предусмотрены процедуры RegisterXXXX, где ХХХХ - имя соответствующего модуля. Например, процедура RegisterDialogs осуществляет регистрацию всех неабстрактных объектов модуля Dialogs, т.е. TButton, TCluster, TInputLine и т.д.; процедура RegisterViews регистрирует объекты модуля Views и т.д.

20.3. СОЗДАНИЕ И УДАЛЕНИЕ ПОТОКА

Для работы с потоками в Turbo Vision предусмотрен абстрактный тип TStream и три его потомка - TDOSStream, TBufStream и TEMSStream.

Объект TDOSStream реализует небуферизованный доступ к файлу или файловому устройству. Буферизация потока означает использование некоторого промежуточного буфера, в который помещаются данные перед физической записью их на диск или сразу после физического чтения с диска. Буферизация позволяет согласовать формат данных с размерами дискового сектора и обычно значительно ускоряет доступ к потоку, поэтому в большинстве случаев в программах используется буферизованная версия потока, реализуемая объектом TBufStream.

Объект TEMSStream обеспечивает доступ к отображаемой памяти компьютера, оборудованного специальной EMS-платой (для ПК с процессорами 80386 и более поздними EMS-память может эмулироваться). Запись объектов в EMS-памятъ и чтение их из нее осуществляется с предельно возможной скоростью, однако содержимое этой памяти разрушается после выключения компьютера. Таким образом, TEMSStream используется для временного хранения данных с целью минимизации времени доступа к ним. Если в программе предполагается частое обращение к потоку, имеет смысл скопировать его в EMS-память в начале работы программы и перенести хранящиеся в нем данные на диск перед завершением работы.

В каждом из объектов TXXXStream предусмотрен свой конструктор Init, с помощью которого создается экземпляр нужного потока. Ниже описывается формат вызова каждого конструктора.

Constructor TDOSStream.Init(FileName: FNameStr; Mode: Word);

Здесь FileName - имя дискового файла или файлового устройства, Mode - способ доступа к данным.

Параметр FileName может содержать полное имя файла с указанием диска и маршрута поиска. Параметр Mode определяет способ доступа к данным. Для задания этого параметра можно использовать следующие константы, определенные в модуле Objects:

const

stCreat = $ЗС00; {Создать файл}

stOpenRead = $3D00; {Открыть файл только для чтения}

stOpenWrite = $3D01; {Открыть файл только для записи}

stOpen = $3D02; {Открыть файл для чтения и записи}

Constructor TBufStream(FileName: FNameStr; Mode: Word; Size: Word);

Здесь FileName, Mode - см. выше; Size - размер буфера в байтах.

Размер буфера выбирается равным размеру дискового сектора (512 байт) или размеру кластера (п*512, и = 1, 2, 4, 8, ...). Минимальные потери времени обеспечивает размер буфера, равный размеру кластера диска (кластер - минимальная порция дискового пространства, выделяемая каждому файлу). При работе с гибкими дисками размер кластера обычно равен одному или двум секторам, для жесткого диска этот размер зависит от общей емкости диска и чаще всего равен 4 или 8 секторам. Если Вы не знаете размеры кластера диска, с которым будет связан поток, установите Size = 512.

Constructor TEMSStream.Init(MinSize, MaxSize: LongInt);

Здесь MinSize, MaxSize определяют соответственно минимальный и максимальный размеры блока, который будет передаваться в EMS-память. Параметр MaxSize имеет смысл только при использовании драйвера EMS-памяти, версии меньше 4.0: в этом случае попытка разместить в расширенной памяти блок, больше MaxSize, вызовет ошибку; при использовании версии драйвера 4.0 и выше в памяти можно разместить блок любого размера, в этом случае параметр MaxSize можно опускать.

После завершения работы с потоком следует удалить его экземпляр - это аналогично тому, как Вы закрываете дисковый файл после его использования. Для удаления потока нужно обратиться к его методу Done, например:

Dispose(PMyStream, Done);

Здесь PMyStream - указатель на экземпляр потока, размещенный в куче. Если Вы не использовали указатель на поток (т.е. если экземпляр потока размещен в обычной переменной), для удаления потока используется вызов

MyStrearn.Done;

(MyStrem - экземпляр потока).

В ходе реализации процедуры TStream.Done очищается внутренний буфер (если использовался буферизованный поток); закрывается файл и уничтожается экземпляр потока.

20.4. РАБОТА С ПОТОКОМ

Базовый объект TStream реализует три метода, используемых для непосредственной работы с потоком. Метод TStream.Put предназначен для передачи объектов в поток и выполняет приблизительно такие же функции, как стандартная файловая процедура Write. Метод TStream.Get используется для чтения объектов из потока, его аналогом является процедура Read. Наконец, с помощью метода TStream.Error анализируется состояние потока после завершения некоторой операции: если обнаружена ошибка при обмене данными с потоком, вызывается этот метод, который по умолчанию просто устанавливает признаки ошибки в информационных полях TStream.Status и TStream.Errorlnfo. Приблизительным аналогом метода TStream.Error служит стандартная файловая функция IOResult.

Сразу же замечу, что в случае возникновения ошибки все последующие операции с потоком блокируются до тех пор, пока не будет вызван метод TStream.Reset.

Методы Put и Get практически никогда не перекрываются: для реализации операций с потоком они обращаются к виртуальным методам Store и Load, которые должны быть определены в каждом объекте, если только этот объект помещается в поток или считывается из него. Главное назначение методов Put и Get состоит в обеспечении полиморфизма потока за счет контроля регистрационных номеров объектов. Методы Load и Store никогда не вызываются прямо, но только из методов Put и Get, т.к. они ничего не знают о регистрационных номерах и не могут работать в полиморфных потоках.

20.4.1. Методы Put и Get

Чтобы поместить объект в поток, нужно обратиться к методу Put, передав ему в качестве параметра инициированный экземпляр объекта. Например:

var

MyStream: TBufStream;{Экземпляр потока} 

MyWindow: TMyWindow;{Экземпляр объекта}

.....

MyStream.Put(MyWindow);{Помещаем объект в поток}

Предварительно объект должен быть зарегистрирован обращением к RegisterType, а поток - инициирован с помощью TXXXStream.Init.

Метод Put вначале отыскивает объект в регистрационном списке, создаваемом процедурой RegisterType, и получает из этого списка регистрационный номер объекта и адрес его метода Store. Затем в поток записывается регистрационный номер и вызывается метод Store, который делает остальное, т.е. копирует в поток все поля объекта.

По такой же схеме работает и метод Get: вначале он считывает из потока регистрационный номер объекта, затем отыскивает его в регистрационном списке и вызывает соответствующий конструктор Load. Конструктор размещает в динамической памяти экземпляр считываемого объекта, а затем считывает из потока все его поля. Результатом работы Get является нетипизированный указатель на вновь созданный и инициированный объект. Например:

type

MyStream: TBufStream;{Экземпляр потока} 

PWindow: PMyWindow;{Указатель на экземпляр объекта}

.....

PWindow := MyStream.Get;{Получаем объект из потока}

Заметим, что количество считываемых из потока данных и тип ТВМ, который назначен вновь созданному объекту, определяется не типом PWindow (см. выше), а регистрационным номером, полученным из потока. Вы можете ошибочно поместить в левой части оператора присваивания указатель на объект другого типа и Turbo Vision не сможет предупредить Вас об этом!

Методы Put и Get позволяют автоматически сохранять в потоке и получать из него сложные объекты (группы). Эта возможность реализуется внутри методов Store и Load.

20.4.2. Методы Store и Load

Метод Store осуществляет запись данных в поток. Для этого он использует метод низкого уровня Write, передавая ему в качестве параметров имя записываемого поля и длину поля в байтах. Заметим, что Вам нет нужды записывать все поля объекта: для записи наследуемых полей достаточно просто обратиться к методу Store объекта-родителя. Ваш метод Store должен записывать только те поля, которые добавляются к полям родителя. Если, например, создан объект

type

TMyDialog = object (TDialog) 

St: String;{Новое поле}

Procedure Store(var S: TStream); Virtual;

.....

end ;

то метод TMyDialog.Store может иметь такую реализацию:

Procedure TMyDialog.Store(var S: TStream); 

begin

TDialog.Store(S); {Сохраняем наследуемые поля} 

SA.Write(St, SizeOf(St)); {Сохраняем новое поле} 

end;

Аналогичным образом реализуется и конструктор Load: с помощью обращения к низкоуровневому методу TStream.Read он получает из потока только дополнительные поля и только в том порядке, как они были записаны в поток методом Store:

Constructor TMyDialog.Load(var S: TStream); 

begin

TDialog.Load(S); {Получаем наследуемые поля} 

S^.Read(St, SizeOf(St)); {Получаем новое поле} 

end;

Вы должны тщательно следить за соответствием методов Store и Load: метод Load' должен прочитать ровно столько байт и строго в той последовательности, сколько байт и в какой последовательности поместил в поток метод Store. В Turbo Vision нет средств контроля За правильностью считываемых данных!

Если Ваш объект - группа, следует включить в него поля-указатели на каждый из элементов и использовать методы PutSubViewPtr и GetSubViewPtr соответственно для записи в поток и чтения из него. Например:

type

TMyDialog = object (TDialog)

St: String; {Текстовое поле}

PB: PButton; {Указатель на кнопку}

Procedure Store(var S: TStream); Virtual;

Constructor Load(var S: TStream);

.....

end;

Procedure TMyDialog.Store(var S: TStream); 

begin

TDialog.Store(S); {Сохраняем наследуемые поля} 

S^.write(ST, SizeOf(St)); {Сохраняем текстовое поле} 

PutSubViewPtr(S, PB) ; {Сохраняем кнопку}

end;

Constructor TMyDialog.Load(var S: TStream);

begin

TDialog.Load(S); {Получаем наследуемые поля} 

S^.Read(St, SizeOf(St)); {Получаем тестовое поле} 

GetSubViewPtr(S, PB); {Получаем кнопку}

end;

20.4.3. Обработка ошибок

При обнаружении ошибки поток вызывает свой метод TStream.Error, который определяет необходимую реакцию программы. По умолчанию этот метод просто записывает информацию об ошибке в поля TStream.Status и TStream.ErrorInfo.

Поле Status определяет тип ошибки, в соответствии со следующими константами модуля Objects:

const

stOk = 0; {Нет ошибки}

stError =-1; {Ошибка доступа}

stInitError =-2; {Ошибка инициации потока}

stReadError =-3; {Чтение за концом потока}

stWriteError =-4; {Нельзя расширить поток}

stGetError =-5; (Get для незарегистрированного объекта}

stPutError =-6; {Put для незарегистрированного объекта}

Поле ErrorInfo определено только для Status - -5 или Status - -6: в первом случае оно содержит регистрационный номер, полученный из потока и не обнаруженный в регистрационном списке; во втором - смещение ТВМ незарегистрированного объекта, который программа пытается поместить в поток.

Сразу после обнаружения ошибки Turbo Vision блокирует все операции с потоком до тех пор, пока аварийная ситуация не будет сброшена обращением к методу TStream.Reset.

20.4.4. Прямой доступ к потокам

Поток имеет методы, имитирующие файловые процедуры прямого доступа к дисковому файлу.

С помощью функции GetPos программа может получить текущую позицию в потоке, т.е. номер байта, начиная с которого будет осуществляться очередная операция с потоком (первый байт потока имеет номер 0).

Метод Seek (Pos: LongInf) перемещает текущую позицию в потоке в байт Pos от начало потока.

Метод GetSize возвращает общий размер потока в байтах.

С помощью метода Truncate можно удалить из потока все данные, начиная с текущей позиции до конца потока.

Как видим, эти процедуры можно использовать только в том случае, если создать вне потока индексную коллекцию, содержащую начальные позиции в потоке для каждого из сохраняемых в нем объектов. Такая коллекция используется в ресурсах, поэтому для обеспечения прямого доступа к потоку лучше использовать файл ресурсов (см. гл.21).

20.4.5. Использование потоков с произвольными данными

Хотя потоки спроектированы в основном для работы с объектами, Вы можете использовать их для хранения не только полей объектов, но и любых других данных. При этом не следует обращаться к методам Put и Get, так как они предполагают доступ к объектам. Вместо этого Вы должны обратиться к низкоуровневым процедурам Write и Read.

Следующая простая программа использует поток для сохранения десяти случайных целых чисел: 

Uses Objects; var

S: TBufStream; {Экземпляр потока}

k, j : Integer; 

begin

WriteLn('Запись в поток:');

S.lnit('Test.dat', stCreate, 512); {Создаем поток}

for k := 1 to 10 do

begin

j := Random(l00); {Получаем случайное целое} 

Write(j:8); {Выводим на экран} 

S.Write(j,2) {Помещаем в поток} 

end; 

Done; {Удаляем поток}

S.lnit('Test.dat', stOpenRead, 512); 

WriteLn;

WriteLn('Чтение из потока:'); 

for k := 1 to 10 do 

begin

S.Read(j,2); {Получаем целое из потока} 

Write (j:8) {Выводим на экран} 

end; 

S.Done; 

WriteLn 

end.

Для простоты в программу не включены средства контроля ошибок. В ходе прогона программы в текущем каталоге диска будет создан файл TEST.DАТ размером в 20 байт, а на экран будут выведены две строки с одинаковыми числами.